package com.xt.core.dao.hibernate;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import java.sql.SQLException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import org.hibernate.HibernateException;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;

import com.xt.core.dao.BasicDao;
import com.xt.core.exception.ObjectCanNotSaveException;
import com.xt.core.helper.page.Pager;
import com.xt.core.helper.data.HibernateFormatUtil;

/**
 * Generic Hibernate DAO implementation (Data Access Object) with common methods to CRUD POJOs.
 *
 * <p>Extend this interface if you want typesafe (no casting necessary) DAO's for your
 * domain objects.
 *
 * <p/>
 * <p>To register this class in your Spring context file, use the following XML.
 * <pre>
 *      &lt;bean id="fooDao" class="com.xt.dao.hibernate.BasicDaoImpl"&gt;
 *          &lt;constructor-arg value="com.xt.dmmy.data.User"/&gt;
 *      &lt;/bean&gt;
 * </pre>
 *
 * @author txiao
 * @version Revision: 1.0, Date: 23.09.2009
 * @param <T> a type variable
 * @param <PK> the primary key for that type
 */
public class BasicDaoImpl<T, PK extends Serializable> implements BasicDao<T, PK>{

	/**
     * Log variable for all child classes. Uses LogFactory.getLog(getClass()) from Commons Logging
     */
    protected final Log log = LogFactory.getLog(getClass());
    private Class<T> persistentClass;
    private HibernateTemplate hibernateTemplate;
    private SessionFactory sessionFactory;
	
    /**
     * Constructor that takes in a class to see which type of entity to persist.
     * Use this constructor when subclassing.
     *
     * @param persistentClass the class type you'd like to persist
     */
	public BasicDaoImpl(Class<T> persistentClass) {
		this.persistentClass = persistentClass;
	}
    
    /**
     * Constructor that takes in a class and sessionFactory for easy creation of DAO.
     *
     * @param persistentClass the class type you'd like to persist
     * @param sessionFactory  the pre-configured Hibernate SessionFactory
     */
    public BasicDaoImpl(final Class<T> persistentClass, SessionFactory sessionFactory) {
        this.persistentClass = persistentClass;
        this.sessionFactory = sessionFactory;
        this.hibernateTemplate = new HibernateTemplate(sessionFactory);
    }

    /** basic functions */
    
    
    public HibernateTemplate getHibernateTemplate() {
        return this.hibernateTemplate;
    }

    public SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    @Autowired
    @Required
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
        this.hibernateTemplate = new HibernateTemplate(sessionFactory);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> getAll() {
        return hibernateTemplate.loadAll(this.persistentClass);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> getAllDistinct() {
        Collection result = new LinkedHashSet(getAll());
        return new ArrayList(result);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public T get(PK id) {
        T entity = (T) hibernateTemplate.get(this.persistentClass, id);

        if (entity == null) {
            log.warn("Uh oh, '" + this.persistentClass + "' object with id '" + id + "' not found...");
            throw new ObjectRetrievalFailureException(this.persistentClass, id);
        }

        return entity;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public boolean exists(PK id) {
        T entity = (T) hibernateTemplate.get(this.persistentClass, id);
        return entity != null;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public T save(T object) {
        return (T) hibernateTemplate.merge(object);
    }

    /**
     * {@inheritDoc}
     */
    public void remove(PK id) {
        hibernateTemplate.delete(this.get(id));
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> findByNamedQuery(String queryName, Map<String, Object> queryParams) {
        String[] params = new String[queryParams.size()];
        Object[] values = new Object[queryParams.size()];
        
        int index = 0;
        for (String s : queryParams.keySet()) {
            params[index] = s;
            values[index++] = queryParams.get(s);
        }

        return hibernateTemplate.findByNamedQueryAndNamedParam(queryName, params, values);
    }

	
	/*
     * Add your function here
     */
	
	/**
	 * save or update the object, this function can replace save or update
	 * 
	 * @param T ojbect the object to be saved or updated
	 */
	public void saveorupdate(T object) throws ObjectCanNotSaveException{
		try{
			hibernateTemplate.saveOrUpdate(object);
			hibernateTemplate.flush();
		}catch(org.hibernate.exception.ConstraintViolationException cve){
			throw new ObjectCanNotSaveException("Object already excists.");
		}catch(Exception e){
			throw new ObjectCanNotSaveException("Can not save object: " + e.getMessage());
		}
	}

	/**
	 * get the entity's name
	 * 
	 * @return the entity name
	 */
	private String getEntityName(){
		//String entityName = this.persistentClass.getSimpleName();  
		String entityName = this.persistentClass.getName();
		if (entityName != null && !"".equals(entityName)) {  
			return entityName;  
		}  
		return null;
	}

	
	/**
	 * get the total rows without conditions
	 * 
	 * @return the total rows
	 */
	public int getTotalRows(){
		return getTotalRows((Set<LinkedHashMap<String, Object>>)null);
	}

	
	/**
	 * get the total rows of results after the condition
	 * 
	 * @param queryParams the parameters as HashMap<key, value>
	 * @return the total rows after the condition
	 */
	@SuppressWarnings("unchecked")
	public int getTotalRows(Set<LinkedHashMap<String, Object>> queryParams){

		DetachedCriteria crit = DetachedCriteria.forEntityName(getEntityName());
		crit = HibernateFormatUtil.buildQuery(crit, queryParams);
		crit.setProjection(Projections.rowCount());
		
		List<Integer> results = (List<Integer>) hibernateTemplate.findByCriteria(crit);
        if(results==null)
        	return 0;
        else
        	return  results.get(0).intValue();
        
	}
	
	
	/**
	 * get total rows directly by hql querystring
	 * be used together with getOnePageByQueryString() 	
	 * 
	 * @param queryString hql querystring (normaly only "from xxx" is enough)
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public long getTotalRows(String queryString){
		List<Long> results = (List<Long>) hibernateTemplate.find(queryString);
        if(results==null)
        	return 0;
        else
        	return  results.get(0).longValue();
	}
	
	
	
	/**
	 * get all the results after the condition and order 
	 * 
	 * @param queryParams  	the query parameters
	 * @param order		order by
	 * @return  the results as list
	 */
	public List<T> getAll(Set<LinkedHashMap<String, Object>> queryParams, LinkedHashMap<String, String> order){
		
		return getOnePage(-1,-1, queryParams, order);
	}
	
	/**
	 * get the result of the given page 
	 * if startRow=-1 and pageSize=-1, return all the results
	 * 
	 * @param startRow the first row in page.
	 * @param pageSize the page size 
	 */
	public List<T> getOnePage(int pageSize, int startRow){
		return getOnePage(pageSize, startRow, null, null);
	}
	
	public List<T> getOnePage(Pager pager){
		if(pager==null)
			return getOnePage(-1, -1);
		else
			return getOnePage(pager.getPageSize(), pager.getStartRow());
	}
	
	public List<T> getOnePage(Pager pager, Set<LinkedHashMap<String, Object>> queryParams, LinkedHashMap<String, String> order){
		if(pager==null)
			return getOnePage(-1, -1, queryParams, order);
		else
			return getOnePage(pager.getPageSize(), pager.getStartRow(),  queryParams, order);
	}
	
	
	/**
	 * for the case User wants to use own query string
	 * 
	 * @param queryString queryString
	 * @param pageSize    page size
	 * @param startRow    the start row
	 * @return the results as list
	 */
	@SuppressWarnings("unchecked")
	public List<T> getOnePage( String queryString, int pageSize, int startRow){
		
		final String querySql = queryString;
		final int pageSize1=pageSize;
        final int startRow1=startRow;
        
        return (List<T>)hibernateTemplate.executeFind(new HibernateCallback(){
            public List<T> doInHibernate(Session session) throws HibernateException, SQLException {
	            Query query=session.createQuery(querySql);	
                if(startRow1 != -1 && pageSize1 != -1) {
	                query.setFirstResult(startRow1);
	                query.setMaxResults(pageSize1);
                }
                return (List<T>) query.list();
            }
        });
	}
	
	/**
	 * get results using HQL
	 * @queryString the HQL
	 * @param pager the pager object
	 * @return the results as list
	 */
	public List<T> getOnePage(String queryString, Pager pager){
		if(pager==null)
			return getOnePage(queryString, -1, -1);
		else
			return getOnePage(queryString, pager.getPageSize(), pager.getStartRow());
	}
	
	
	/**
	 * get results using spring's hibernateTemplate.findByCriteria();
	 * @param pageSize the pagesize
	 * @param startRow the start row
	 * @param queryParams query parameters - eq
	 * @param order order parameters
	 * @return results as list
	 */
	@SuppressWarnings("unchecked")
	private List<T> getOnePageByCri(int pageSize, int startRow, Set<LinkedHashMap<String, Object>> queryParams, LinkedHashMap<String, String> order){

		DetachedCriteria crit = DetachedCriteria.forEntityName(getEntityName());
		crit = HibernateFormatUtil.buildQuery(crit, queryParams);
		crit = HibernateFormatUtil.buildOrder(crit, order);
		
		List<T> results;
		if(startRow != -1 && pageSize != -1) {
			results = (List<T>) hibernateTemplate.findByCriteria(crit, startRow, pageSize);
		}
		else{
			results = (List<T>) hibernateTemplate.findByCriteria(crit);
		}
		if(results == null || results.size() == 0){
			log.debug("getOnePageByCri(): result size: 0");
			return null;
		}else {
			log.debug("getOnePageByCri(): result size: " + results.size());
			return results;
		}
	}
	
	/**
	 * under construction!!!
	 * get one page after conditions, generate HQL String
	 * this function only supports simple query with one object, string properties
	 * if you want to conplex functions, you must compose query string yourself
	 * 
	 * @param pageSize the size of page
	 * @param startRow the start row
	 * @param queryParams  query parameters as HashMap <fieldname, value>
	 * @param orderBy order condition as HashMap <fieldName, value>
	 * @return the results as list. If none found, return null.
	 */
	@SuppressWarnings("unchecked")
	private List<T> getOnePageByHQL(int pageSize, int startRow, LinkedHashMap<String, String> queryParams, LinkedHashMap<String, String> order){
		
		final int pageSize1=pageSize;
        final int startRow1=startRow;
		final String orderBy = HibernateFormatUtil.buildOrderby(order);
		final String paramSql = HibernateFormatUtil.buildQueryParam(queryParams);

		return (List<T>)hibernateTemplate.executeFind(new HibernateCallback(){
            public List<T> doInHibernate(Session session) throws HibernateException, SQLException {

            	String sql = "FROM " + getEntityName() + " as o ";
                if(paramSql!=null && !"".equals(paramSql))
                	sql += paramSql;
                if(orderBy!=null && !"".equals(orderBy))
                	sql += orderBy;
                //log.debug("sql: " + sql);	
                Query query=session.createQuery(sql);
                if(startRow1 != -1 && pageSize1 != -1) {
	                query.setFirstResult(startRow1);
	                query.setMaxResults(pageSize1);
                }
                
                List<T> results = (List<T>) query.list();
        		if(results == null || results.size() == 0){
        			log.debug("getOnePageByHQL(): result size: 0");
        			return null;
        		}else {
        			log.debug("getOnePageByHQL(): result size: " + results.size());
        			return results;
        		}
            }
        });
	}
	
	/**
	 * get results using spring's hibernateTemplate.findByCriteria();
	 * if you want to switch to other mode(byhql), switch it here
	 * 
	 * @param pageSize the pagesize
	 * @param startRow the start row
	 * @param queryParams query parameters - eq, like, ge, le
	 * @param order order parameters
	 * @return results as list
	 */
	public List<T> getOnePage(int pageSize, int startRow, Set<LinkedHashMap<String, Object>> queryParams, LinkedHashMap<String, String> order){
		
		//return getOnePageByHQL(pageSize,startRow, queryParams, orderby);
		return getOnePageByCri(pageSize,startRow, queryParams, order);
	}

}
